package com.tianji.promotion.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tianji.api.cache.CategoryCache;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.*;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.entity.dto.CouponFormDTO;
import com.tianji.promotion.entity.dto.CouponIssueFormDTO;
import com.tianji.promotion.entity.po.Coupon;
import com.tianji.promotion.entity.po.CouponScope;
import com.tianji.promotion.entity.po.UserCoupon;
import com.tianji.promotion.entity.query.CouponQuery;
import com.tianji.promotion.entity.vo.CouponDetailVO;
import com.tianji.promotion.entity.vo.CouponPageVO;
import com.tianji.promotion.entity.vo.CouponScopeVO;
import com.tianji.promotion.entity.vo.CouponVO;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.enums.ObtainType;
import com.tianji.promotion.enums.UserCouponStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.ICouponService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

import static com.tianji.promotion.enums.CouponStatus.*;
import static com.tianji.promotion.enums.CouponStatus.UN_ISSUE;

/**
 * <p>
 * 优惠券的规则信息 服务实现类
 * </p>
 *
 * @author lemon
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon> implements ICouponService {

    private final ICouponScopeService couponScopeService;

    private final IExchangeCodeService exchangeCodeService;

    private final StringRedisTemplate stringRedisTemplate;

    private final IUserCouponService userCouponService;

    private final CategoryCache categoryCache;


    /**
     * 新增优惠券
     *
     * @param dto
     */
    @Transactional
    @Override
    public void saveCoupon(CouponFormDTO dto) {
        // 1.dto转po 保存优惠券信息
        Coupon coupon = BeanUtils.copyBean(dto, Coupon.class);
        this.save(coupon);

        // 保存后coupon的id会自动赋值 因为是雪花算法生成的id
        // 2.判定是否限定了范围  没有限定范围则直接返回
        if (!dto.getSpecific()) {
            return;
        }

        // 3.需要校验scope的类型是否合法
        List<Long> scopes = dto.getScopes();
        if (CollUtils.isEmpty(scopes)) {
            throw new BadRequestException("优惠券的作用范围不能为空");
        }

        // 4.保存优惠券的作用范围信息 coupon_scope  批量新增
        List<CouponScope> scList = new ArrayList<>();
        for (Long scope : scopes) {
            CouponScope couponScope = new CouponScope();
            couponScope.setCouponId(coupon.getId());
            couponScope.setBizId(scope);
            couponScope.setType(1);
            scList.add(couponScope);
        }
        couponScopeService.saveBatch(scList);

    }

    /**
     * 分页查询优惠券
     *
     * @param query
     * @return
     */
    @Override
    public PageDTO<CouponPageVO> pageCoupon(CouponQuery query) {
        Integer status = query.getStatus();
        String name = query.getName();
        Integer type = query.getType();
        // 1.分页查询
        Page<Coupon> page = lambdaQuery()
                .eq(type != null, Coupon::getDiscountType, type)
                .eq(status != null, Coupon::getStatus, status)
                .like(StringUtils.isNotBlank(name), Coupon::getName, name)
                .page(query.toMpPageDefaultSortByCreateTimeDesc());
        // 2.处理VO
        List<Coupon> records = page.getRecords();
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page);
        }
        List<CouponPageVO> list = BeanUtils.copyList(records, CouponPageVO.class);
        // 3.返回
        return PageDTO.of(page, list);
    }

    /**
     * 修改优惠券
     *
     * @param id
     * @param dto
     */
    @Override
    public void updateCoupon(Long id, CouponFormDTO dto) {
        // 根据id查询优惠券信息
        Coupon coupon = getById(id);
        if (coupon == null) {
            throw new BizIllegalException("优惠券不存在");
        }
        // 1.修改优惠券信息
        BeanUtils.copyProperties(dto, coupon);
        updateById(coupon);
    }

    /**
     * 删除优惠券
     *
     * @param id
     */
    @Override
    public void deleteById(Long id) {
        // 查询优惠券信息
        Coupon coupon = getById(id);
        if (coupon == null || coupon.getStatus() != DRAFT) {
            throw new BadRequestException("优惠券不存在或者优惠券正在使用中");
        }

        // 1.删除优惠券信息
        boolean success = remove(new LambdaQueryWrapper<Coupon>()
                .eq(Coupon::getId, id)
                .eq(Coupon::getStatus, DRAFT));
        if (!success) {
            throw new BadRequestException("优惠券不存在或者优惠券正在使用中");
        }
        // 3.删除优惠券对应限定范围
        if (!coupon.getSpecific()) {
            return;
        }
        couponScopeService.remove(new LambdaQueryWrapper<CouponScope>().eq(CouponScope::getCouponId, id));

    }

    /**
     * 发放优惠券
     *
     * @param couponId
     * @param dto
     */
    @Override
    public void beginIssue(Long couponId, CouponIssueFormDTO dto) {
        log.info("发放优惠券 线程名称:{}", Thread.currentThread().getName());
        // 校验
        if (!Objects.equals(couponId, dto.getId())) {
            throw new BadRequestException("优惠券id不一致");
        }
        Coupon coupon = getById(couponId);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }
        // 首先是校验优惠券的状态
        // 必须是
        // 2.判断优惠券状态，是否是暂停或待发放
        if (coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != PAUSE) {
            throw new BizIllegalException("优惠券状态错误！");
        }
        // 3.判断是否是立刻发放
        LocalDateTime issueBeginTime = dto.getIssueBeginTime();
        LocalDateTime now = LocalDateTime.now();
        // 3.1 如果是立刻发放 则判断是否在有效期内
        // 表示立刻发放             TODO 注意后面的判断条件
        boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);
        /*方式一：
        4.2.更新状态
        if (isBegin) {
            coupon.setIssueBeginTime(dto.getIssueBeginTime() == null ? now : dto.getIssueBeginTime());
            coupon.setIssueEndTime(dto.getIssueEndTime());
            coupon.setStatus(ISSUING.getValue());
            coupon.setTermDays(dto.getTermDays());
            coupon.setTermBeginTime(dto.getTermBeginTime());
            coupon.setTermEndTime(dto.getTermEndTime());
        } else {
            coupon.setIssueBeginTime(dto.getIssueBeginTime());
            coupon.setIssueEndTime(dto.getIssueEndTime());
            coupon.setTermDays(dto.getTermDays());
            coupon.setTermBeginTime(dto.getTermBeginTime());
            coupon.setTermEndTime(dto.getTermEndTime());
            coupon.setStatus(UN_ISSUE.getValue());
        }
        // 4.3.写入数据库
        this.updateById(coupon);*/

        // 方式二：
        Coupon c = BeanUtils.copyBean(dto, Coupon.class);
        // 4.2.更新状态
        if (isBegin) {
            c.setStatus(ISSUING);
            c.setIssueBeginTime(now);
        } else {
            c.setStatus(UN_ISSUE);
        }
        // 4.3.写入数据库
        this.updateById(c);

        // 优化后的代码
        // TODO 5.入如果优惠券是立刻发放，将优惠券信息(优惠券id,领券开始时间结束时间。发行总数量，限领数量) hash结构 存入redis
        if (isBegin) {
            // 优惠券id
            String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId;
            stringRedisTemplate.opsForHash().put(key, "issueBeginTime", String.valueOf(DateUtils.toEpochMilli(c.getIssueBeginTime())));
            stringRedisTemplate.opsForHash().put(key, "issueEndTime", String.valueOf(DateUtils.toEpochMilli(c.getIssueEndTime())));
            stringRedisTemplate.opsForHash().put(key, "total", c.getTotalNum().toString());
            stringRedisTemplate.opsForHash().put(key, "userLimit", c.getUserLimit().toString());

            /*
            方式二： 这个就是用map来存储 一次性全部存储
            Map<String, String> map = new HashMap<>();
            map.put("issueBeginTime", String.valueOf(DateUtils.toEpochMilli(c.getIssueBeginTime())));
            map.put("issueEndTime", String.valueOf(DateUtils.toEpochMilli(c.getIssueEndTime())));
            map.put("total", c.getTotalNum().toString());
            map.put("userLimit", c.getUserLimit().toString());
            stringRedisTemplate.opsForHash().putAll(key, map);
            */

        }

        // 6.如果为指定发放 并且转为为待发放，则需要生成优惠券兑换码
        if (coupon.getStatus() == DRAFT && coupon.getObtainWay() == ObtainType.ISSUE) {
            // 给兑换码生成过期时间
            coupon.setIssueEndTime(c.getIssueEndTime()); // 领取的截止时间
            exchangeCodeService.asyncGenerateCode(coupon);
        }
    }

    /**
     * 查询发放中的优惠券列表-用户端
     *
     * @return
     */
    @Override
    public List<CouponVO> queryIssuingCoupons() {
        // 1.状态是发放中  并且是 手动领取
        List<Coupon> couponList = lambdaQuery()
                .eq(Coupon::getStatus, ISSUING)
                .eq(Coupon::getObtainWay, ObtainType.ISSUE)
                .list();
        if (CollUtils.isEmpty(couponList)) {
            return CollUtils.emptyList();
        }
        Long userId = UserContext.getUser();

        // 先获取couponId
        List<Long> couponIds = couponList.stream().map(Coupon::getId).collect(Collectors.toList());
        // 查询我的优惠券
        List<UserCoupon> userCouponList = userCouponService.lambdaQuery()
                .in(UserCoupon::getCouponId, couponIds)
                .eq(UserCoupon::getUserId, userId)
                .list();

        // 2.2.统计当前用户对优惠券的已经领取数量
        Map<Long, Long> issuedMap = userCouponList.stream()
                .collect(Collectors.groupingBy(UserCoupon::getCouponId, Collectors.counting()));
        // 2.3.统计当前用户对优惠券的已经领取并且未使用的数量
        Map<Long, Long> unusedMap = userCouponList.stream()
                .filter(uc -> uc.getStatus() == UserCouponStatus.UNUSED)
                .collect(Collectors.groupingBy(UserCoupon::getCouponId, Collectors.counting()));

        // 3.po封装为vo
        List<CouponVO> voList = new ArrayList<>();
        for (Coupon coupon : couponList) {
            CouponVO couponVO = BeanUtils.copyBean(coupon, CouponVO.class);

            // 优惠券还有，并且自己领取的数量小于限制数量
            Long orDefault = issuedMap.getOrDefault(coupon.getId(), 0L);
            boolean isAvailable = coupon.getIssueNum() < coupon.getTotalNum() && orDefault < coupon.getUserLimit();
            couponVO.setAvailable(isAvailable); // 是否可领取

            // 统计用户优惠券未使用数量，并且是在有效期内
            Long orDefault1 = unusedMap.getOrDefault(coupon.getId(), 0L);
            couponVO.setReceived(orDefault1 > 0); // 是否可以使用

            voList.add(couponVO);
        }
        return voList;
    }

    /**
     * 暂停发放优惠券接口
     *
     * @param id
     */
    @Override
    public void pauseIssue(Long id) {
        // 1.查询旧优惠券
        Coupon coupon = getById(id);
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在");
        }

        // 2.当前券状态必须是未开始或进行中
        CouponStatus status = CouponStatus.of(coupon.getStatus().getValue());
        if (status != UN_ISSUE && status != ISSUING) {
            // 状态错误，直接结束
            return;
        }

        // 3.更新状态
        boolean success = lambdaUpdate()
                .set(Coupon::getStatus, PAUSE)
                .eq(Coupon::getId, id)
                .in(Coupon::getStatus, UN_ISSUE, ISSUING)
                .update();
        if (!success) {
            // 可能是重复更新，结束
            log.error("重复暂停优惠券");
        }

        // 4.删除缓存(TODO 目前是没有的，后面再加)
        stringRedisTemplate.delete(PromotionConstants.COUPON_CACHE_KEY_PREFIX + id);
    }

    /**
     * 查询优惠券详情信息-管理端
     *
     * @param id
     * @return
     */
    @Override
    public CouponDetailVO queryCouponById(Long id) {
        // 1.查询优惠券
        Coupon coupon = getById(id);
        // 2.转换VO
        CouponDetailVO vo = BeanUtils.copyBean(coupon, CouponDetailVO.class);
        if (vo == null || !coupon.getSpecific()) {
            // 数据不存在，或者没有限定范围，直接结束
            return vo;
        }
        // 3.查询限定范围
        List<CouponScope> scopes = couponScopeService.lambdaQuery()
                .eq(CouponScope::getCouponId, id)
                .list();
        if (CollUtils.isEmpty(scopes)) {
            return vo;
        }
        // 调用category服务，查询分类名称
        List<CouponScopeVO> collect = scopes.stream()
                .map(CouponScope::getBizId)
                // 目前只针对三级分类
                .map(cateId -> new CouponScopeVO(cateId, categoryCache.getNameByLv3Id(cateId)))
                .collect(Collectors.toList());
        vo.setScopes(collect);
        return vo;
    }

    /**
     * 定时扫描上架优惠券，并存入redis
     */
    @Transactional
    @Override
    public void openCouponTask() {
        // 条件，状态：待发放  当前时间在发放时间范围内
        List<Coupon> couponList = this.lambdaQuery()
                .eq(Coupon::getStatus, DRAFT)
                .le(Coupon::getIssueBeginTime, LocalDateTime.now())
                .ge(Coupon::getIssueEndTime, LocalDateTime.now())
                .list();

        if (CollUtils.isEmpty(couponList)) {
            return;
        }

        // 将他们存入redis
        // 使用hash结构存储
        String keyPrefix = PromotionConstants.COUPON_CACHE_KEY_PREFIX;
        Map<String, String> map = new HashMap<>();
        for (Coupon coupon : couponList) {
            map = new HashMap<>();
            map.put("issueBeginTime", String.valueOf(DateUtils.toEpochMilli(coupon.getIssueBeginTime())));
            map.put("issueEndTime", String.valueOf(DateUtils.toEpochMilli(coupon.getIssueEndTime())));
            map.put("total", coupon.getTotalNum().toString());
            map.put("userLimit", coupon.getUserLimit().toString());
            stringRedisTemplate.opsForHash().putAll(keyPrefix + coupon.getId(), map);
        }
        // 将couponList的状态修改为发放中
        couponList.forEach(coupon -> {
            coupon.setStatus(ISSUING);
        });

        // 批量更新
        boolean flag = this.updateBatchById(couponList);

        if (!flag) {
            // 将redis中的数据删除
            couponList.forEach(coupon -> {
                stringRedisTemplate.delete(keyPrefix + coupon.getId());
            });
            log.error("批量更新优惠券状态失败");
        }


    }

    /**
     * 定时扫描下架优惠券，并从redis中删除，并且修改优惠券状态
     */
    @Transactional
    @Override
    public void clearCouponTask() {
        // 就从redis中看哪些的优惠券过期了
        // 1.获取所有的key
        Set<String> keys = stringRedisTemplate.keys(PromotionConstants.COUPON_CACHE_KEY_PREFIX + "*");
        if (CollUtils.isEmpty(keys)) {
            return;
        }
        // 2.遍历key，获取value
        List<String> keyList = new ArrayList<>(keys);
        List<Object> values = stringRedisTemplate.opsForHash().multiGet(String.valueOf(keyList), Arrays.asList("issueEndTime", "total", "userLimit"));

        // 3.遍历value，判断是否过期
        List<Coupon> couponList = new ArrayList<>();
        for (int i = 0; i < values.size(); i++) {
            Map<String, String> map = (Map<String, String>) values.get(i);
            var issueEndTime = Long.parseLong(map.get("issueEndTime"));
            Integer total = Integer.valueOf(map.get("total"));
            Integer userLimit = Integer.valueOf(map.get("userLimit"));
            if (issueEndTime < System.currentTimeMillis()) {
                // 过期了，删除redis中的数据
                stringRedisTemplate.delete(keyList.get(i));
                // 修改优惠券状态
                Coupon coupon = new Coupon();
                coupon.setId(Long.valueOf(keyList.get(i).substring(PromotionConstants.COUPON_CACHE_KEY_PREFIX.length())));
                coupon.setStatus(FINISHED);
                coupon.setTotalNum(total);
                coupon.setUserLimit(userLimit);
                couponList.add(coupon);
            }
        }
        // 4.批量更新
        if (CollUtils.isEmpty(couponList)) {
            return;
        }
        boolean flag = this.updateBatchById(couponList);
        if (!flag) {
            // 将redis中的数据弄回去
            Map<String, String> map = null;
            for (Coupon coupon : couponList) {
                map = new HashMap<String, String>();
                map.put("issueBeginTime", String.valueOf(DateUtils.toEpochMilli(coupon.getIssueBeginTime())));
                map.put("issueEndTime", String.valueOf(DateUtils.toEpochMilli(coupon.getIssueEndTime())));
                map.put("total", coupon.getTotalNum().toString());
                map.put("userLimit", coupon.getUserLimit().toString());
                stringRedisTemplate.opsForHash().putAll(PromotionConstants.COUPON_CACHE_KEY_PREFIX + coupon.getId(), map);
            }
            log.error("批量更新优惠券状态失败");
        }
    }

    /**
     * 定时扫描那些过提前两天就要到期的，从redis里面扫描
     */
    @Override
    public void tiXingCouponTask() {
        // 1.获取所有的key
        Set<String> keys = stringRedisTemplate.keys(PromotionConstants.COUPON_CACHE_KEY_PREFIX + "*");
        if (CollUtils.isEmpty(keys)) {
            return;
        }
        // 2.遍历key，获取value
        List<String> keyList = new ArrayList<>(keys);
        // 取出全部的value

        List<Object> objects = stringRedisTemplate.opsForHash().multiGet(String.valueOf(keyList), Arrays.asList("issueEndTime", "total", "userLimit"));
        // 3.遍历value，判断是否过期
        List<Coupon> couponList = new ArrayList<>();
        for (int i = 0; i < objects.size(); i++) {
            Map<String, String> map = (Map<String, String>) objects.get(i);
            var issueEndTime = Long.parseLong(map.get("issueEndTime"));

            if (issueEndTime < System.currentTimeMillis() + 2 * 24 * 60 * 60 * 1000) {
                // 表示这里的优惠券还有两天就要过期了，要去提醒用户
                // 1.获取优惠券id
                Long couponId = Long.valueOf(keyList.get(i).substring(PromotionConstants.COUPON_CACHE_KEY_PREFIX.length()));
                // TODO
            }
        }
    }
}
